Self Hosted Git Server: How to

Author: Hayden Hargreaves
Published: 05/22/2025
Background
Version control is one of the most powerful tools used by developers, and Git is the most widely adopted version control system (vcs). However, when it comes to hosting Git, everyone does it a little differently. Most people use GitHub or even GitLab. Large companies typically host their own for an added layer of safety and security. That is exactly what this guide will cover, but on a smaller scale of course!
Before we dig into the details, what exactly does it mean to "roll your own version control" or "host your own git server"? Well, it's simple, we are going to use a server of our own to deploy an application that serves as a web-UI and hub for our Git repositories. Before you freak out, we are not going to actually write any code or build the application, there are countless open-source options available for free that "home-labbers" such as myself. In this guide, we will be using Gitea due to its ease of use and strong support.
NOTE: As an added benefit, it was written in Go and is accepting contributions!
Requirements
There are only a few things you will need to roll your own Git server. The most important is a server, duh! This can be a virtual private server (VPS), an EC2 instance from AWS, or your own hardware. Whatever you have will work, but my recommendation is to purchase your own hardware. I have a large server built of old gaming PC parts, but even a simple Raspberry Pi will due!
Once you have a server and root access (you will need to create and modify a user) you are about 99% there! I assume that because you are reading this you have a personal computer. You will need SSH access to your server via a personal computer. This article will walk you through using Ansible to configure your server (which requires SSH access). This guide assumes you are using a Debian or Ubuntu based Linux distro.
Finally, the last "requirement" is optional, but highly recommended: a personal domain and a Cloudflare account. Regardless of whether you have a domain or not, you will be able to access your Git server from your local network. But, if you want access remotely securely, it is best to get your hands on a domain. Using Cloudflare allows us access to their tunnels which will allow us to expose local ports safely. More details regarding these tunnels will come later.
NOTE: There are other ways to access your server remotely without Cloudflare tunnels, but I will not cover that here.
Preview
Before continuing, please make sure you have everything you need to get started. Following these steps, in order will allow you to go from 0 to self hosting your server with relative ease!
- Install docker-compose: We will be running the server in a docker container
- Create the git user: Creating a new user will allow you to access the server using the git user
- Configure docker-compose: This is the easiest way to install Gitea
- Configure the server: The server can be configured via the web UI
- Configure SSH access: The magic begins to happen here
- Configure remote access: This is the final step that ties the bow on the whole system
Disclaimer
It is assumed that you already have a basic understanding of Ansible and have a basic config setup. As this is not an Ansible guide, I will not go into much detail there. However, many of these commands are easy to understand and can be used as normal shell commands.
For those who have ansible already configured on their system, we will be using the common roles pattern for directories and files. A directory structure that looks something like this will yield the best results:
.
├── ansible.cfg
├── inventory
│ ├── group_vars
│ │ └── main.yml
│ ├── hosts.yml
│ └── host_vars
│ └── gophernest.yml
├── playbooks
│ ├── common_setup.yml
│ └── docker_apps.yml
├── requirements.yml
└── roles
├── cloudflared
│ ├── files
│ │ ├── 3c522d3a-5f24-4645-b4ca-695c66e05ef3.json
│ │ ├── cert.pem
│ │ └── cloudflared
│ ├── handlers
│ │ └── main.yml
│ ├── tasks
│ │ └── main.yml
│ ├── templates
│ │ └── config.yml.j2
│ └── vars
│ └── main.yml
├── docker
│ ├── handlers
│ │ └── main.yml
│ ├── tasks
│ │ └── main.yml
│ └── vars
│ └── main.yml
└── git
├── README.md
├── tasks
│ └── main.yml
├── templates
│ └── docker-compose.yml.j2
└── vars
└── main.yml
File paths will be provided at each step, if you are following along, you can use the structure above to create an exact copy. RECOMMENDED!
Install Docker Compose
The first requirement is to ensure that docker compose is installed. This can be done by updating the
roles/docker/tasks/main.yml
file to contain the following task.
# roles/docker/tasks/main.yml
...
- name: Install Docker Compose
get_url:
url: https://github.com/docker/compose/releases/latest/download/docker-compose-linux-x86_64 # Modify system accordingly
dest: /usr/local/bin/docker-compose
mode: '0755'
become: true
tags:
- docker
- compose
Also, make sure you have a working installation of Docker on your system. Those not using Ansible can reference the docs which provide a distro-specific installation guide.
You can test that this has worked successfully by running the docker compose command:
docker-compose --version
Create the Git User
Now its time to create the user that will handle the server and manage the data. It is best practice to create
a new user with permission only for this application, to follow the principal of least privilege. This can
be done very easily by updating the roles/git/tasks/main.yml
file to contain the following tasks:
# roles/git/tasks/main.yml
...
- name: Create git user
user:
name: git
password: "{{ GIT_USER_PASSWORD }}"
shell: /bin/bash
state: present
become: true
tags:
- git
- user
- name: Add git user to the required groups
user:
name: git
groups: sudo,docker
append: yes
state: present
become: true
tags:
- git
- groups
The password can be set directly here, or you can update the roles/git/vars/main.yml
file to contain an entry
for the password. Ansible knows to look here when we use the syntax provided above.
# roles/git/vars/main.yml
...
GIT_USER_PASSWORD: "super secret password" # use `mkpasswd -m sha-512 'password'`
For non Ansible users, this can be done with the typical Linux commands:
useradd -m -s /bin/bash git
passwd git
usermod -aG sudo git
usermod -aG docker git
Configure Docker Compose
We will now create the required docker-compose file to start the application. The file should be placed in the
new git users home directory, /home/git/docker-compose.yml
. This can be done with a single task in the same
playbook as previous.
# roles/git/tasks/main.yml
...
- name: Copy docker-compose file to the server
template:
src: docker-compose.yml.j2
dest: /home/git/docker-compose.yml
owner: git
group: git
mode: "0644"
become: true
tags:
- git
- docker
In order for this to work, we must also provide the docker-compose.yml.j2
file in the templates
directory.
# roles/git/templates/docker-compose.yml.j2
networks:
gitea:
external: false
services:
server:
image: docker.gitea.com/gitea:1.23.8
container_name: gitea
environment:
- USER=git
- USER_UID=1001 # As the git user, run `id` to get UID and GID values
- USER_GID=1002
restart: always # Allows the container to start when the server boots
networks:
- gitea
volumes:
- ./gitea:/data
- /etc/timezone:/etc/timezone:ro
- /etc/localtime:/etc/localtime:ro
ports:
- "4000:3000"
- "222:22" # Adjust the host ports as necessary, host:container
To do this manually, simply create a file /home/git/docker-compose.yml
with the content in the above template.
Configure the Server
We will use the Gitea web-UI to configure the server, but first we must start the server. With ansible, we can create the following task in the same location as the previous tasks (starting to notice a trend I hope).
# roles/git/tasks/main.yml
...
- name: Start Docker compose application
community.docker.docker_compose_v2:
files: /home/git/docker-compose.yml
project_src: /home/git
state: present
pull: always
become: true
tags:
- git
- start
Or you can run the docker compose command from the git users home directory /home/git
:
docker-compose up -d # Use -d if you want it to run in the background, as a daemon
Now you can access our server locally using the local address of your server on port 4000 (or whatever you set
in the docker compose file). For example, http://192.168.1.2:4000
. You should see a configuration wizard, if
so, you are almost done!
Feel free to customize these settings as you see fit, but ensure you follow the provided directions.
- Do not change the port's, HTTP or SSH, these are internal ports! To change the external ports, update the hosts ports in the docker container.
- Leave the user as git, we set this up for a reason!
- Disable the self registration toggle in the advanced settings section (at the bottom).
Configure Local Access
Your Git server is live! You have made it through the hardest part, the rest is easy. Access your server via HTTP works but it's not the best but it works. So, now we will configure our local system to use SSH key authentication. First you will need an SSH key, but I will leave that up to you to figure out.
Once you have your key, you need to add it to your Gitea server. The process is very similar to added an SSH key to
GitHub, Settings > SSH/GPG Keys > Add Key. Then paste the content of your *.pub
file into the content field.
Finally, we need to configure our local machine to use this key when we access our Git server. Update your .gitconfig
file to contain an entry similar to this:
Host gitea # Update as needed
Port 222 # Update as needed
User git
HostName 192.168.1.2 # Use your address here, we will change this later
IdentityFile ~/.ssh/key
Much of these details will change when we setup our server to run on our domain, but for now, give them a try and adjust them accordingly.
When you attempt to clone a repo (for example) you will use the URL:
git clone git@gitea:<username>/<repo>.git
Notice, we use gitea here as the host. Since this is how we configured our config to route to our server.
Side Note: Local Access
If you would only like access to this server from your local network then you can stop at this step.
Configure Remote Access
We will be using an existing Cloudflare tunnel, but I will not go into detail about setting one
up. It is a pretty simple process that can be done without too much explanation. So, I will assume
you have a tunnel up and running. All we have to do, is route an endpoint from our local machine
to sub domain in our Cloudflare tunnel. By now, this should be easy for you, since you have setup
and configured your tunnel already (hopefully). But to remind you, you must add a record to your
config.yml
file, wherever it is on your system.
...
ingress:
- hostname: git.domain.net # Enter your domain here
service: http://localhost:4000 # Update the port as needed
...
But that is not all, the last step you need to do is add a CNAME record in your Cloudflare
DNS dashboard. This can be done manually, like you have before, or by creating an Ansible task
as follows. This will be its own role, cloudflared
# roles/cloudflared/tasks/main.yml
...
# Update domain to your own
- name: Configure cloudflare Tunnel DNS Record (CNAMEs) for *.domain.net
community.general.cloudflare_dns:
zone: "domain.net"
record: "{{ item }}.domain.net"
type: "CNAME"
value: "{{ tunnel_id }}.cfargotunnel.com"
state: present
proxied: true
api_token: "{{ cloudflare_api_key }}"
loop: "{{ domain_cnames }}"
tags:
- cloudflared
- cnames
Like in the previous steps, we will need some variables in our roles/cloudflared/vars/main.yml
file.
...
tunnel_id: "tunnel_id" # Enter your tunnel id here
cloudflare_api_key: "api_key" # Enter your API key here
# Include as many sub domains as you want, for now, we just need git
gophernest_cnames:
- git
- ...
You may notice, we are using an API key. This is a free and simple process which is described here in the Cloudflare docs. The key will allow us to update DNS records using their API.
Now, you will be able to access the web interface of your git server at git.yourdomain.net! However, we cannot use this domain with SSH to complete actions, such as cloning. At this state, you can clone (or do other actions) using a URL that looks like this:
git clone git@your_servers_ip:username/repo.git
NOTE: Tunneling TCP or UDP is more complex and will not be apart of this guide.
But this is not ideal, nobody wants to use their server IP address to access their git server! So, what can we do? Well, the best option is to simply use a DNS the old way. In your Cloudflare DNS panel, create an A entry with a value of whatever subdomain you want (we will need this later) and the content being your servers IP address. For this record, make sure to deselect the proxied check box. What this will do is route the traffic from subdomain.domain.net to the IP address.
But why do we need that? Having a route will allow us to configure our SSH config to use this URL and
access our git server via SSH without much effort. Update the previous record we created in our ~/.ssh/config
file to look more like this:
Host gitea # Remeber this value!
Port 222
User git
HostName subdomain.domain.net # This is the only change
IdentityFile ~/.ssh/key
You should now be able to complete SSH actions using the gitea domain! An example would look like this:
git clone git@gitea:username/repo.git
Simple right! We are just about done, the last thing we need to do is update our Gitea config to use this
new route in the frontend. You may notice that your web UI will provide a different value when you press
clone on a repo (for example). To fix this, all you need to do edit your config file at
/home/git/gitea/gitea/conf/app.ini
. You will replace the line starting with SSH_DOMAIN
to match whatever
value you labeled your SSH key to use.
For example:
[server]
...
SSH_DOMAIN = gitea
You may also edit any other domain values you see to match your own domain. These values will update the text fields that are provided to the user when actions are taken. After restarting the docker compose image you will see the updates live!
For those using Ansible, this config change can be done using a simple task added to your roles/git/tasks/main.yml
file.
# roles/git/tasks/main.yml
...
- name: Update the ssh domain in the config file if it exists
replace:
path: /home/git/gitea/gitea/conf/app.ini
regexp: '^SSH_DOMAIN = (.*)'
replace: 'SSH_DOMAIN = gitea'
become: true
tags:
- git
- config
NOTE: I have also found it helpful to append this new task to the bottom of the git role as a safety measure.
# roles/git/tasks/main.yml
...
- name: Restart Docker compose application
community.docker.docker_compose_v2:
files: /home/git/docker-compose.yml
project_src: /home/git
state: restarted
pull: always
become: true
tags:
- git
- restart
This will ensure the application is in its most recent state after each update.
Conclusion
You now have your own version control server running in your home server! This solution should not replace GitHub in your workflow, some projects belong in the public eye. Your favorite projects are a great way for future employers to see what kind of things you can do! But some things, like your Ansible config files, or your NixOS configuration, does not need to be public. Your home git server is a great place for those projects! Just remember to make them private repos in Gitea ;)